﻿/* 
 * Copyright (c) 2008 Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using DotNetOpenId;
using DotNetOpenId.RelyingParty;
using ExtensionLoader;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using HttpServer;

public struct UserAuthentication
{
    public IAuthenticationResponse OpenIdResponse;
    public UUID Token;
}

namespace AssetServer.Extensions
{
    public class OpenIdAuth : IExtension<AssetServer>, IAuthenticationProvider
    {
        const string HTML_HEADER = "<html><head><title>Asset Server Authentication</title></head><body>";
        const string HTML_FOOTER = "</body></html>";
        const string AUTH_FORM = "<form method=\"post\"><label for=\"openid_identifier\">OpenID URL:</label> <input type=\"text\" name=\"openid_identifier\" id=\"openid_identifier\"/> <input type=\"submit\" value=\"Authenticate\"></form>";
        
        AssetServer server;
        ApplicationMemoryStore openidStore = new ApplicationMemoryStore();
        DoubleDictionary<UUID, Uri, Uri> authStore = new DoubleDictionary<UUID, Uri, Uri>();

        public OpenIdAuth()
        {
        }

        public bool Start(AssetServer server)
        {
            this.server = server;

            server.HttpServer.AddHandler("get", null, @"^/authenticate", WebGetHandler);
            server.HttpServer.AddHandler("post", "application/x-www-form-urlencoded", @"^/authenticate", WebAuthHandler);

            server.HttpServer.AddHandler("post", "application/json", @"^/authenticate", AuthStartHandler);
            server.HttpServer.AddHandler(null, null, @"^/authenticate", AuthHandler);
            return true;
        }

        public void Stop()
        {
        }

        public void AddIdentifier(UUID token, Uri identifier)
        {
            lock (authStore)
            {
                // If this token already existed in the store, remove it first
                if (authStore.ContainsKey(token))
                {
                    Uri id;
                    if (authStore.TryGetValue(token, out id))
                    {
                        authStore.Remove(token, id);
                    }
                }
                // We only support one session per identifier, so check if this
                // identifier has another session open and remove it
                else if (authStore.ContainsKey(identifier))
                {
                    Logger.Log.Warn("[OpenIdAuth] Executing slow remove query for existing session, identifier: " +
                        identifier.ToString());
                    authStore.Remove(identifier);
                }

                authStore.Add(token, identifier, identifier);
            }
        }

        public bool RemoveIdentifier(UUID token)
        {
            lock (authStore)
            {
                Uri identifier;
                if (authStore.TryGetValue(token, out identifier))
                    return authStore.Remove(token, identifier);
                else
                    return false;
            }
        }

        public bool TryGetIdentifier(UUID token, out Uri identifier)
        {
            lock (authStore)
                return authStore.TryGetValue(token, out identifier);
        }

        bool AuthStartHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            try
            {
                OSDMap map = OSDParser.DeserializeJson(request.Body) as OSDMap;
                Uri claimedIdentifier = map["identifier"].AsUri();

                if (claimedIdentifier != null)
                {
                    OpenIdRelyingParty rp = new OpenIdRelyingParty(openidStore, null, null);

                    string baseURL = String.Format("{0}://{1}", request.Uri.Scheme, request.Uri.Authority);
                    Realm realm = new Realm(baseURL);

                    IAuthenticationRequest authRequest = rp.CreateRequest(claimedIdentifier, realm, request.Uri);

                    response.Status = authRequest.RedirectingResponse.Code;
                    for (int i = 0; i < authRequest.RedirectingResponse.Headers.AllKeys.Length; i++)
                    {
                        string key = authRequest.RedirectingResponse.Headers.AllKeys[i];
                        response.AddHeader(key, authRequest.RedirectingResponse.Headers[key]);
                    }
                }
                else
                {
                    response.Status = HttpStatusCode.BadRequest;
                }
            }
            catch (Exception ex)
            {
                Logger.Log.Warn(ex.Message);
                response.Status = HttpStatusCode.InternalServerError;
                response.Reason = ex.Message;
            }

            return true;
        }

        bool AuthHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            UUID authToken = UUID.Zero;

            if (!String.IsNullOrEmpty(request.Uri.Query))
            {
                NameValueCollection query = HttpUtility.ParseQueryString(request.Uri.Query);

                OpenIdRelyingParty rp = new OpenIdRelyingParty(openidStore, request.Uri, query);
                if (rp.Response != null)
                {
                    if (rp.Response.Status == AuthenticationStatus.Authenticated)
                    {
                        authToken = UUID.Random();
                        AddIdentifier(authToken, new Uri(rp.Response.ClaimedIdentifier.ToString()));
                        response.Cookies.Add(new ResponseCookie("authToken", authToken.ToString(), DateTime.Now + TimeSpan.FromDays(7)));
                    }
                    else if (rp.Response.Exception != null)
                    {
                        response.Reason = rp.Response.Exception.Message;
                    }
                }
            }
            else
            {
                authToken = Utils.GetAuthToken(request);
            }

            Uri identifier;
            if (authToken != UUID.Zero && server.AuthenticationProvider.TryGetIdentifier(authToken, out identifier))
            {
                OSDMap osd = new OSDMap(2);
                osd.Add("identifier", OSD.FromUri(identifier));
                osd.Add("auth_token", OSD.FromUUID(authToken));

                byte[] responseData = Encoding.UTF8.GetBytes(OSDParser.SerializeJson(osd).ToJson());

                response.ContentLength = responseData.Length;
                response.ContentType = "application/json";

                response.Body.Write(responseData, 0, responseData.Length);
                response.Body.Flush();
            }
            else
            {
                // Default response
                response.Status = HttpStatusCode.Forbidden;
            }

            return true;
        }

        bool WebGetHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            const string HTML_HEADER = "<html><head><title>Asset Server Authentication</title></head><body>";
            const string HTML_FOOTER = "</body></html>";
            const string AUTH_FORM = "<form method=\"post\"><label for=\"openid_identifier\">OpenID URL:</label> <input type=\"text\" name=\"openid_identifier\" id=\"openid_identifier\"/> <input type=\"submit\" value=\"Authenticate\"></form>";

            if (!String.IsNullOrEmpty(request.Uri.Query))
            {
                NameValueCollection query = HttpUtility.ParseQueryString(request.Uri.Query);

                OpenIdRelyingParty rp = new OpenIdRelyingParty(openidStore, request.Uri, query);
                if (rp.Response != null && rp.Response.Status == AuthenticationStatus.Authenticated)
                {
                    UUID token = UUID.Random();
                    AddIdentifier(token, new Uri(rp.Response.ClaimedIdentifier.ToString()));
                    response.Cookies.Add(new ResponseCookie("authToken", token.ToString(), DateTime.Now + TimeSpan.FromDays(7)));
                }

                response.Status = HttpStatusCode.Redirect;
                response.AddHeader("Location", String.Format("{0}://{1}{2}", request.Uri.Scheme, request.Uri.Authority, request.Uri.AbsolutePath));
            }
            else
            {
                UUID token;
                Uri identifier;
                RequestCookie authCookie = request.Cookies != null ? request.Cookies["authToken"] : null;
                if (authCookie != null && UUID.TryParse(authCookie.Value, out token) && authStore.TryGetValue(token, out identifier))
                {
                    string responseStr = String.Format("{0}Authenticated as {1}, token is {2}{3}",
                        HTML_HEADER, identifier, token, HTML_FOOTER);
                    byte[] data = Encoding.UTF8.GetBytes(responseStr);

                    response.Body.Write(data, 0, data.Length);
                }
                else
                {
                    string responseStr = HTML_HEADER + AUTH_FORM + HTML_FOOTER;
                    byte[] data = Encoding.UTF8.GetBytes(responseStr);
                    response.Body.Write(data, 0, data.Length);
                }
            }

            return true;
        }

        bool WebAuthHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            byte[] buffer = new byte[request.ContentLength];
            int pos = 0;

            while (pos < buffer.Length)
                pos += request.Body.Read(buffer, 0, buffer.Length);

            string queryString = HttpUtility.UrlDecode(buffer, Encoding.UTF8);
            NameValueCollection query = System.Web.HttpUtility.ParseQueryString(queryString);
            string[] openidIdentifiers = query.GetValues("openid_identifier");
            if (openidIdentifiers.Length == 1)
            {
                Identifier identifier;
                if (UriIdentifier.TryParse(openidIdentifiers[0], out identifier))
                {
                    OpenIdRelyingParty rp = new OpenIdRelyingParty(openidStore, null, null);

                    string baseURL = String.Format("{0}://{1}", request.Uri.Scheme, request.Uri.Authority);
                    Realm realm = new Realm(baseURL);

                    IAuthenticationRequest authRequest = rp.CreateRequest(identifier, realm, request.Uri);

                    response.Status = authRequest.RedirectingResponse.Code;
                    for (int i = 0; i < authRequest.RedirectingResponse.Headers.Keys.Count; i++)
                    {
                        string key = authRequest.RedirectingResponse.Headers.Keys[i];
                        response.AddHeader(key, authRequest.RedirectingResponse.Headers[key]);
                    }
                }
            }

            return true;
        }
    }
}
